Preliminary Network Visualization with R

Learning Outcomes

  • Understanding Quarto and RMarkdown documents
  • Understanding functions and arguments
  • Loading a data set: read_csv()
  • Peeking at a data set: head(), tail(), glimpse()
  • Wrangling data frames with {tidyverse} commands: filter(), rename()
  • Creating a data frame from scratch with field names: data.frame()
  • Running functions from {visNetwork} and {igraph} to create networks and understanding argument values: visNetwork(), graph_from_data_frame(), E(), V()
  • Writing out {igraph} objects to a file, e.g., in “.csv” or “.graphml” format
  • Calculating node-level centrality metrics to describe network structure
  • Calculating clustering coefficients

Introduction

Today, we are going to do some preliminary exercises for working with network data using R and RStudio. You should use a “Quarto” or “RMarkdown” document for organizing your work and notes to get practice using that approach to doing “reproducible research”. Both of these are simple text documents that allow you to mix together narrative text and code to generate formatted, dynamic output. It also allows you to easily manage notes and run code.

Quarto/RMarkdown and R Basics

  • Use the File > New File > Quarto Document… or File > New File > R Markdown… command to create new Quarto or R Markdown documents.

  • Use hashtags (#) to format headers for section titles. The more #’s, the smaller the header.

  • Use markdown syntax to format text

    • Wrap text in asterisks (e.g., *italics*) to create italicized text
    • Wrap text in double asterisks (e.g., **bold**) to create bold text
    • Wrap text in carats (e.g., ^superscript^) to create superscript text
    • Wrap text in tildes (e.g., ~subscript~^) to create subscript text
    • Wrap text in double tildes (e.g., ~~strikethrough~~) to create strikethrough text
    • Wrap text in back ticks (e.g., `inline code`) to create inline code
  • Chunks of R code are included in code blocks that start and end with three back ticks. The opening line of the code block should also include the designator {r}, which tells the rendering engine to run the code using R (as opposed to Python, etc.)

```{r}
# example code block syntax
print("this is a code block")
```
[1] "this is a code block"
  • Run code by highlighting it in your document and hitting command-RETURN or command-ENTER. This sends the code to the R console and executes it.

  • A whole block of code can be run by clicking the green right arrow at the top of the code block

  • Comments can be included within R code blocks by prefacing them with #

  • Use the Render (for Quarto documents) or Knit (for RMarkdown documents) to create nicely formatted reports based on your notes and code. These commands read through your text file and create an HTML or PDF file that incorporates your syntax, runs the code in your code blocks, and inserts the output of running that code into the report.

Today’s Exercise

Load in a dataset

We will use a slightly different version of the dataset we were working with last time: social connections among characters in the Game of Thrones saga, but only from book one.

# read in edges and vertices as data frames/tibbles from .csv files using the {tidyverse} package
library(tidyverse)
f <- file.choose()
e <- read_csv(f, col_names = TRUE)
head(e)
# A tibble: 6 × 5
  Source                          Target             Type       weight  book
  <chr>                           <chr>              <chr>       <dbl> <dbl>
1 Addam-Marbrand                  Jaime-Lannister    Undirected      3     1
2 Addam-Marbrand                  Tywin-Lannister    Undirected      6     1
3 Aegon-I-Targaryen               Daenerys-Targaryen Undirected      5     1
4 Aegon-I-Targaryen               Eddard-Stark       Undirected      4     1
5 Aemon-Targaryen-(Maester-Aemon) Alliser-Thorne     Undirected      4     1
6 Aemon-Targaryen-(Maester-Aemon) Bowen-Marsh        Undirected      4     1
tail(e)
# A tibble: 6 × 5
  Source           Target          Type       weight  book
  <chr>            <chr>           <chr>       <dbl> <dbl>
1 Tyrion-Lannister Varys           Undirected      3     1
2 Tyrion-Lannister Willis-Wode     Undirected      4     1
3 Tyrion-Lannister Yoren           Undirected     10     1
4 Tywin-Lannister  Varys           Undirected      4     1
5 Tywin-Lannister  Walder-Frey     Undirected      8     1
6 Waymar-Royce     Will-(prologue) Undirected     18     1
glimpse(e)
Rows: 684
Columns: 5
$ Source <chr> "Addam-Marbrand", "Addam-Marbrand", "Aegon-I-Targaryen", "Aegon…
$ Target <chr> "Jaime-Lannister", "Tywin-Lannister", "Daenerys-Targaryen", "Ed…
$ Type   <chr> "Undirected", "Undirected", "Undirected", "Undirected", "Undire…
$ weight <dbl> 3, 6, 5, 4, 4, 4, 9, 5, 13, 34, 5, 4, 10, 3, 5, 3, 12, 11, 6, 4…
$ book   <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …

Create a graph using the {visNetwork} package…

We will first use the visNetwork package to do a quick visualization of the network. Start by loading in the package…

library(visNetwork)

{visNetwork} requires separate data frames for nodes and edges, so we create those from our dataset…

# we first create a data frame with edges and weights as a simple copy of the dataset we have loaded in
edges <- e

Because this dataset is so large, we’re going to limit it to only those edges with a high weight…

# pick out only edges with high weight
edges <- filter(edges, weight >= 50)

From this, we then create a data frame of vertices by selecting all the unique names in either the “Source” or “Target” columns…

vertices <- data.frame(
  id = unique(c(edges$Source,edges$Target)),
  label = unique(c(edges$Source, edges$Target))
  # the label argument allows us to display node names
)

We then change some column names to meet some requirements for the visNetwork() function, e.g., we change “Source” and “Target” to “from” and “to” and we make the “Type” column start with a lowercase letter…

edges <- rename(edges, from = Source, to = Target, type = Type)

We also add new columns for “width”, “label”, and “title” to improve our visualization…

edges <- mutate(edges,
  width = weight / max(weight) * 20, # scale the width
  label = weight, # label the edges with their weights
  title = paste("Weight:", weight) # tooltip for the edges
)

Now, we use the visNetwork() function to create and visualize the network… Note that this creates an interactive graph where we can select and move vertices around!

# create the visNetwork graph
visNetwork(nodes = vertices, edges = edges) %>%
  visEdges(smooth = TRUE) %>%
  visNodes(size = 20) %>%
  # set a seed for reproducible layout
  visLayout(randomSeed = 42)

Questions

Looking at this graph, answer the following questions…

  • How many components are in the graph?
  • What is the diameter of the largest component?
  • Which node(s) has the highest degree? What is that degree?
  • If we initially filter our dataset by a higher edge weight (e.g., 60), how do these answers change? What about using a lower edge weight (e.g., 40)?

Create the same graph using the {igraph} package…

# Load the igraph package
library(igraph)

# use the `graph_from_data_frame()` function to create a graph from our data frames of edges and vertices
graph <- graph_from_data_frame(
  d = edges,
  vertices = vertices,
  directed = FALSE)
graph
IGRAPH 3d62e27 UNW- 20 18 -- 
+ attr: name (v/c), label (v/c), type (e/c), weight (e/n), book (e/n),
| width (e/n), label (e/n), title (e/c)
+ edges from 3d62e27 (vertex names):
 [1] Arya-Stark        --Sansa-Stark      Bran-Stark        --Jon-Snow        
 [3] Bran-Stark        --Luwin            Bran-Stark        --Robb-Stark      
 [5] Bronn             --Tyrion-Lannister Catelyn-Stark     --Eddard-Stark    
 [7] Cersei-Lannister  --Eddard-Stark     Cersei-Lannister  --Robert-Baratheon
 [9] Daenerys-Targaryen--Drogo            Daenerys-Targaryen--Jorah-Mormont   
[11] Eddard-Stark      --Petyr-Baelish    Eddard-Stark      --Robert-Baratheon
[13] Eddard-Stark      --Varys            Jeor-Mormont      --Jon-Snow        
+ ... omitted several edges

We can use the {igraph} functions E() and V() to get information about the edges and vertices of a graph…

# show edge information
E(graph)
+ 18/18 edges from 3d62e27 (vertex names):
 [1] Arya-Stark        --Sansa-Stark      Bran-Stark        --Jon-Snow        
 [3] Bran-Stark        --Luwin            Bran-Stark        --Robb-Stark      
 [5] Bronn             --Tyrion-Lannister Catelyn-Stark     --Eddard-Stark    
 [7] Cersei-Lannister  --Eddard-Stark     Cersei-Lannister  --Robert-Baratheon
 [9] Daenerys-Targaryen--Drogo            Daenerys-Targaryen--Jorah-Mormont   
[11] Eddard-Stark      --Petyr-Baelish    Eddard-Stark      --Robert-Baratheon
[13] Eddard-Stark      --Varys            Jeor-Mormont      --Jon-Snow        
[15] Joffrey-Baratheon --Sansa-Stark      Jon-Snow          --Robb-Stark      
[17] Jon-Snow          --Samwell-Tarly    Jon-Snow          --Tyrion-Lannister
E(graph)$type
 [1] "Undirected" "Undirected" "Undirected" "Undirected" "Undirected"
 [6] "Undirected" "Undirected" "Undirected" "Undirected" "Undirected"
[11] "Undirected" "Undirected" "Undirected" "Undirected" "Undirected"
[16] "Undirected" "Undirected" "Undirected"
E(graph)$weight
 [1] 104  56  65 112  61  64  69  72 101  75  81 291  61  81  87  53  81  56
E(graph)$width
 [1]  7.147766  3.848797  4.467354  7.697595  4.192440  4.398625  4.742268
 [8]  4.948454  6.941581  5.154639  5.567010 20.000000  4.192440  5.567010
[15]  5.979381  3.642612  5.567010  3.848797
# show vertex information
V(graph)
+ 20/20 vertices, named, from 3d62e27:
 [1] Arya-Stark         Bran-Stark         Bronn              Catelyn-Stark     
 [5] Cersei-Lannister   Daenerys-Targaryen Eddard-Stark       Jeor-Mormont      
 [9] Joffrey-Baratheon  Jon-Snow           Sansa-Stark        Luwin             
[13] Robb-Stark         Tyrion-Lannister   Robert-Baratheon   Drogo             
[17] Jorah-Mormont      Petyr-Baelish      Varys              Samwell-Tarly     
V(graph)$label
 [1] "Arya-Stark"         "Bran-Stark"         "Bronn"             
 [4] "Catelyn-Stark"      "Cersei-Lannister"   "Daenerys-Targaryen"
 [7] "Eddard-Stark"       "Jeor-Mormont"       "Joffrey-Baratheon" 
[10] "Jon-Snow"           "Sansa-Stark"        "Luwin"             
[13] "Robb-Stark"         "Tyrion-Lannister"   "Robert-Baratheon"  
[16] "Drogo"              "Jorah-Mormont"      "Petyr-Baelish"     
[19] "Varys"              "Samwell-Tarly"     

Use the plot.igraph() function to visualize a graph…

l <- layout_in_circle(graph)
plot.igraph(graph,
            vertex.shape="square",
            vertex.size = 5,
            layout = l)

# circle layouts are common, but not always visually informative!

{igraph} includes the option to use various “force-directed” layout algorithms for constructing nicer-looking graphs, where the edges are similar in length and cross each other as little as possible

They work by simulating the graph as a physical system

l <- layout_with_kk(graph)
plot.igraph(graph,
            vertex.shape="square",
            vertex.size = 5,
            edge.label = "",
            layout = l)

# here, edge widths are determined by the "width" variable we defined above, and edge labels are set to be blank

plot.igraph(graph,
            vertex.shape="circle",
            vertex.size = 8,
            edge.width = 4,
            layout = l)

# here, we set a single edge widths, and edge labels come from the "weight" variable

Some other useful {igraph} functions

Other functions from {igraph} can be run to show alternative representations of the graph properties or to return graph properties…

# show adjacency matrix representation
m <- as_adjacency_matrix(graph)
head(m)
6 x 20 sparse Matrix of class "dgCMatrix"
                                                          
Arya-Stark         . . . . . . . . . . 1 . . . . . . . . .
Bran-Stark         . . . . . . . . . 1 . 1 1 . . . . . . .
Bronn              . . . . . . . . . . . . . 1 . . . . . .
Catelyn-Stark      . . . . . . 1 . . . . . . . . . . . . .
Cersei-Lannister   . . . . . . 1 . . . . . . . 1 . . . . .
Daenerys-Targaryen . . . . . . . . . . . . . . . 1 1 . . .
# show adjacency matrix with weights
m <- as_adjacency_matrix(graph, attr = "weight")
head(m)
6 x 20 sparse Matrix of class "dgCMatrix"
                                                                      
Arya-Stark         . . . . . .  . . .  . 104  .   .  .  .   .  . . . .
Bran-Stark         . . . . . .  . . . 56   . 65 112  .  .   .  . . . .
Bronn              . . . . . .  . . .  .   .  .   . 61  .   .  . . . .
Catelyn-Stark      . . . . . . 64 . .  .   .  .   .  .  .   .  . . . .
Cersei-Lannister   . . . . . . 69 . .  .   .  .   .  . 72   .  . . . .
Daenerys-Targaryen . . . . . .  . . .  .   .  .   .  .  . 101 75 . . .
# show edge list
l <- as_edgelist(graph, names = TRUE)
head(l)
     [,1]            [,2]              
[1,] "Arya-Stark"    "Sansa-Stark"     
[2,] "Bran-Stark"    "Jon-Snow"        
[3,] "Bran-Stark"    "Luwin"           
[4,] "Bran-Stark"    "Robb-Stark"      
[5,] "Bronn"         "Tyrion-Lannister"
[6,] "Catelyn-Stark" "Eddard-Stark"    
# list vertices adjacent to each vertex
l <- as_adj_list(graph)
head(l)
$`Arya-Stark`
+ 1/20 vertex, named, from 3d62e27:
[1] Sansa-Stark

$`Bran-Stark`
+ 3/20 vertices, named, from 3d62e27:
[1] Jon-Snow   Luwin      Robb-Stark

$Bronn
+ 1/20 vertex, named, from 3d62e27:
[1] Tyrion-Lannister

$`Catelyn-Stark`
+ 1/20 vertex, named, from 3d62e27:
[1] Eddard-Stark

$`Cersei-Lannister`
+ 2/20 vertices, named, from 3d62e27:
[1] Eddard-Stark     Robert-Baratheon

$`Daenerys-Targaryen`
+ 2/20 vertices, named, from 3d62e27:
[1] Drogo         Jorah-Mormont
# list edges connected to each vertex
l <- as_adj_edge_list(graph) 
head(l)
$`Arya-Stark`
+ 1/18 edge from 3d62e27 (vertex names):
[1] Arya-Stark--Sansa-Stark

$`Bran-Stark`
+ 3/18 edges from 3d62e27 (vertex names):
[1] Bran-Stark--Jon-Snow   Bran-Stark--Luwin      Bran-Stark--Robb-Stark

$Bronn
+ 1/18 edge from 3d62e27 (vertex names):
[1] Bronn--Tyrion-Lannister

$`Catelyn-Stark`
+ 1/18 edge from 3d62e27 (vertex names):
[1] Catelyn-Stark--Eddard-Stark

$`Cersei-Lannister`
+ 2/18 edges from 3d62e27 (vertex names):
[1] Cersei-Lannister--Eddard-Stark     Cersei-Lannister--Robert-Baratheon

$`Daenerys-Targaryen`
+ 2/18 edges from 3d62e27 (vertex names):
[1] Daenerys-Targaryen--Drogo         Daenerys-Targaryen--Jorah-Mormont
# calculate the degree of each vertex and average degree
degree(graph)
        Arya-Stark         Bran-Stark              Bronn      Catelyn-Stark 
                 1                  3                  1                  1 
  Cersei-Lannister Daenerys-Targaryen       Eddard-Stark       Jeor-Mormont 
                 2                  2                  5                  1 
 Joffrey-Baratheon           Jon-Snow        Sansa-Stark              Luwin 
                 1                  5                  2                  1 
        Robb-Stark   Tyrion-Lannister   Robert-Baratheon              Drogo 
                 2                  2                  2                  1 
     Jorah-Mormont      Petyr-Baelish              Varys      Samwell-Tarly 
                 1                  1                  1                  1 
mean(degree(graph)) # average degree
[1] 1.8
# calculate geodesic distance between all pairs ofvertices using weights
geo_d <- distances(graph)
head(geo_d)
                   Arya-Stark Bran-Stark Bronn Catelyn-Stark Cersei-Lannister
Arya-Stark                  0        Inf   Inf           Inf              Inf
Bran-Stark                Inf          0   173           Inf              Inf
Bronn                     Inf        173     0           Inf              Inf
Catelyn-Stark             Inf        Inf   Inf             0              133
Cersei-Lannister          Inf        Inf   Inf           133                0
Daenerys-Targaryen        Inf        Inf   Inf           Inf              Inf
                   Daenerys-Targaryen Eddard-Stark Jeor-Mormont
Arya-Stark                        Inf          Inf          Inf
Bran-Stark                        Inf          Inf          137
Bronn                             Inf          Inf          198
Catelyn-Stark                     Inf           64          Inf
Cersei-Lannister                  Inf           69          Inf
Daenerys-Targaryen                  0          Inf          Inf
                   Joffrey-Baratheon Jon-Snow Sansa-Stark Luwin Robb-Stark
Arya-Stark                       191      Inf         104   Inf        Inf
Bran-Stark                       Inf       56         Inf    65        109
Bronn                            Inf      117         Inf   238        170
Catelyn-Stark                    Inf      Inf         Inf   Inf        Inf
Cersei-Lannister                 Inf      Inf         Inf   Inf        Inf
Daenerys-Targaryen               Inf      Inf         Inf   Inf        Inf
                   Tyrion-Lannister Robert-Baratheon Drogo Jorah-Mormont
Arya-Stark                      Inf              Inf   Inf           Inf
Bran-Stark                      112              Inf   Inf           Inf
Bronn                            61              Inf   Inf           Inf
Catelyn-Stark                   Inf              205   Inf           Inf
Cersei-Lannister                Inf               72   Inf           Inf
Daenerys-Targaryen              Inf              Inf   101            75
                   Petyr-Baelish Varys Samwell-Tarly
Arya-Stark                   Inf   Inf           Inf
Bran-Stark                   Inf   Inf           137
Bronn                        Inf   Inf           198
Catelyn-Stark                145   125           Inf
Cersei-Lannister             150   130           Inf
Daenerys-Targaryen           Inf   Inf           Inf
# calculate unweighted geodesic distance between pairs of vertices, ignoring weights
geo_d <- distances(graph, algorithm = "unweighted")
head(geo_d)
                   Arya-Stark Bran-Stark Bronn Catelyn-Stark Cersei-Lannister
Arya-Stark                  0        Inf   Inf           Inf              Inf
Bran-Stark                Inf          0     3           Inf              Inf
Bronn                     Inf          3     0           Inf              Inf
Catelyn-Stark             Inf        Inf   Inf             0                2
Cersei-Lannister          Inf        Inf   Inf             2                0
Daenerys-Targaryen        Inf        Inf   Inf           Inf              Inf
                   Daenerys-Targaryen Eddard-Stark Jeor-Mormont
Arya-Stark                        Inf          Inf          Inf
Bran-Stark                        Inf          Inf            2
Bronn                             Inf          Inf            3
Catelyn-Stark                     Inf            1          Inf
Cersei-Lannister                  Inf            1          Inf
Daenerys-Targaryen                  0          Inf          Inf
                   Joffrey-Baratheon Jon-Snow Sansa-Stark Luwin Robb-Stark
Arya-Stark                         2      Inf           1   Inf        Inf
Bran-Stark                       Inf        1         Inf     1          1
Bronn                            Inf        2         Inf     4          3
Catelyn-Stark                    Inf      Inf         Inf   Inf        Inf
Cersei-Lannister                 Inf      Inf         Inf   Inf        Inf
Daenerys-Targaryen               Inf      Inf         Inf   Inf        Inf
                   Tyrion-Lannister Robert-Baratheon Drogo Jorah-Mormont
Arya-Stark                      Inf              Inf   Inf           Inf
Bran-Stark                        2              Inf   Inf           Inf
Bronn                             1              Inf   Inf           Inf
Catelyn-Stark                   Inf                2   Inf           Inf
Cersei-Lannister                Inf                1   Inf           Inf
Daenerys-Targaryen              Inf              Inf     1             1
                   Petyr-Baelish Varys Samwell-Tarly
Arya-Stark                   Inf   Inf           Inf
Bran-Stark                   Inf   Inf             2
Bronn                        Inf   Inf             3
Catelyn-Stark                  2     2           Inf
Cersei-Lannister               2     2           Inf
Daenerys-Targaryen           Inf   Inf           Inf
# average distance between pairs of vertices
mean_distance( # uses weights
  graph,
  unconnected = TRUE,
  details = FALSE
)
[1] 130.7551

Calculate and plot the degree distribution…

d <- degree_distribution(graph)

# the following two lines will plot the degree distribution
x <- seq(0, length(d)-1, by = 1)
barplot(d, names = x, xlab = "degree", ylab = "rel freq")

Write out an {igraph} data to a file…

# in .csv format
write_csv(as_long_data_frame(graph), file = "got.csv")

# in .graphml format
write_graph(graph, file = "got.graphml", format = c("graphml"))

Other cool things to explore

Plot an {igraph} object with {visNetwork} directly

visIgraph(graph)
# here, graph is an {igraph} object, and visIgraph() is a function from {visNetwork}

Convert an {igraph} object to a {visNetwork} object

graph <- toVisNetworkData(graph, idToLabel = TRUE)

{visNetwork} options

Below are some examples of interesting {visNetwork} options for tweaking graph layouts (e.g., by editing the physics of layout positioning) and for interacting with a graph…

visNetwork(graph$nodes, graph$edges) %>%
  visPhysics(
    solver = "forceAtlas2Based",
    forceAtlas2Based = list(gravitationalConstant = -30)
    )
visNetwork(graph$nodes, graph$edges) %>%
  visPhysics(solver = "repulsion")
visNetwork(graph$nodes, graph$edges) %>%
  visOptions(
    highlightNearest = list(
      enabled = TRUE,
      hover = TRUE,
      degree = 1,
      hideColor = "rgba(0,0,0,0.05)")
    ) %>%
  visEdges(color = list(inherit = "to"))
visNetwork(graph$nodes, graph$edges) %>% 
  visOptions(manipulation = TRUE) %>%
  visLayout(randomSeed = 42)

Visualizing and working with other datasets from the package {igraphdata}

library(igraphdata)
data("foodwebs") # list containing multiple {igraph} objects
graph <- foodwebs$Narragan
visIgraph(graph)
data("USairports") # a single {igraph} object
graph <- USairports
visIgraph(USairports)

Questions

  • Can you calculate the mean degree and plot the degree distribution for each of the new networks graphed above?

Calculating the “importance” of nodes using centrality measures

Let’s go back to the Game of Thrones graph in {igraph} format…

graph <- graph_from_data_frame(
  d = edges,
  vertices = vertices,
  directed = FALSE)

# we can plot this directly with {visNetwork} and can even call igraph layout routines...
visIgraph(graph) %>%
  visIgraphLayout(layout = "layout_with_kk") %>%
  visLayout(randomSeed = 42)

Now, we calculate several standard node-level metrics of centrality, which are common ways of characterizing the importance of different nodes in the network.

Degree centrality (a.k.a. node degree)
degree <- degree(graph)
Node strength
strength <- strength(graph)
Betweenness
bw_weighted <- betweenness( # with edges weighted...
  graph,
  directed = FALSE
)

E(graph)$weight <- 1

bw_unweighted <- betweenness( # with edges set to a uniform weight
  graph,
  directed = FALSE
)
Eigen centrality
eigen <- eigen_centrality(graph)$vector
Hub and authority scores
hub <- hub_score(graph)$vector

authority <- authority_score(graph)$vector
Closeness centrality
closeness <- closeness(graph)
Reach

“Reach” is calculated as the number of vertices that can be accessed from any particular vertex (“ego”) within a specified numbers of steps from each that vertex, scaled by the total number of possible vertices that could be accessed

The {igraph} functions, ego_size() or neighborhood(), with two arguments (“graph = graph” and “order = k”), return the number of vertices, including “ego”, within a given number of steps, k, from “ego”, thus ego_size(graph, 1) - 1 is equivalent to degree!

We divide this number by the total number of vertices other than “ego” in the graph to calculate reach

# below, we scale reach by the number of vertices other than ego in the graph, which is a measure of how many possible links ego could have

reach_1 <- (ego_size(graph, 1) - 1) / (vcount(graph) - 1)

reach_2 <- (ego_size(graph, 2) - 1) / (vcount(graph) - 1)

reach_3 <- (ego_size(graph, 3) - 1) / (vcount(graph) - 1)

Customizing visualizations

We can put all these measures together in a data frame and add a “shape” variable for visualization in {visNetwork}

centralities <- data.frame(
  id = names(degree),
  label = names(degree),
  degree,
  strength,
  bw_weighted,
  bw_unweighted,
  eigen,
  hub,
  authority,
  closeness,
  reach_1,
  reach_2,
  reach_3,
  shape = "dot") # add a shape attribute so that we can scale shapes

# using {igraph}

l <- layout_with_kk(graph)

plot.igraph(graph,
            layout=l, 
            vertex.size = degree * 3,
            main="Degree")

plot.igraph(graph,
            layout=l, 
            vertex.size= eigen * 15,
            main="Eigen")

plot.igraph(graph,
            layout=l, 
            vertex.size = closeness * 50,
            main="Closeness")

plot.igraph(graph,
            layout=l, 
            vertex.size = bw_weighted * 2,
            main="Betweeness")

# using {visNetwork}
graph <- graph_from_data_frame(
  d = edges,
  vertices = centralities,
  directed = FALSE)

size_var <- "bw_weighted" # try changing which variable to plot by replacing "bw_weighted" with other measures of centrality

centralities <- mutate(centralities, value = centralities[[size_var]])

visNetwork(nodes = centralities, edges = edges) %>%
  visIgraphLayout(layout = "layout_with_kk") %>%
  visLayout(randomSeed = 42)

Transitivity or clustering coefficient

The local transitivity or clustering coefficient of a vertex is a measure of how connected the neighbors of that vertex are to one another. It is calculated as…

\[C_{i}=\frac{2\times e_{i}}{k_{i}(k_{i}-1)}\]

… where \(k_{i}\) = the number of neighbors of vertex \(i\) (i.e., the degree of node \(i\)), and \(e_{i}\) = the edges linking neighbors of vertex \(i\).

Global transitivity is the ratio of the overall count of triangles to connected trios of vertices (both connected and unconnected) in the graph.

The {igraph} function transitivity() can be used to calculate these two clustering coefficients

transitivity(graph, type = "local")
        Arya-Stark         Bran-Stark              Bronn      Catelyn-Stark 
               NaN          0.3333333                NaN                NaN 
  Cersei-Lannister Daenerys-Targaryen       Eddard-Stark       Jeor-Mormont 
         1.0000000          0.0000000          0.1000000                NaN 
 Joffrey-Baratheon           Jon-Snow        Sansa-Stark              Luwin 
               NaN          0.1000000          0.0000000                NaN 
        Robb-Stark   Tyrion-Lannister   Robert-Baratheon              Drogo 
         1.0000000          0.0000000          1.0000000                NaN 
     Jorah-Mormont      Petyr-Baelish              Varys      Samwell-Tarly 
               NaN                NaN                NaN                NaN 
transitivity(graph, type = "global")
[1] 0.2068966

We can also use this function to calculate the average transitivity across vertices…

transitivity(graph, type = "localaverage")
[1] 0.3925926

Transitivity can also be calculated by first converting an {igraph} object to a {network} object using the package {intergraph}, and then using functions bundled in the package {statnet}. To do this conversion, we first install and load the {intergraph} package and then call the conversion function asNetwork()

library(intergraph)
network <- asNetwork(graph)

We then need to install and load {statnet}, which imports functions from two additional useful packages for network analysis, {sna} and {network}…

library(statnet)

# note that we can plot network objects from within the {statnet} package!

# the plotting code below first introduces a new function, `rescale()`, that allows the user to specify a range of values over which to rescale a variable of interest

# then, the `plot.network()` function calls this new function to set vertex sizes...

rescale <- function(variable, low, high) {
  min_d <- min(variable)
  max_d <- max(variable)
  rescaled <- ((high-low) * (variable - min_d)) / (max_d-min_d) + low
  return(rescaled)
}

plot.network(network,
             vertex.cex = rescale(bw_weighted, 1, 6))

The {sna} package loaded with {statnet} includes a function called gtrans(), which calculates global transitivity…

transitivity <- gtrans(network)
transitivity
[1] 0.2068966